系列:30 天用 Google AI 打造臺灣防災速報 App(Day 6/30)
NCDR 給了我們廣度,中央氣象署(CWA)開放資料平台給我們深度,尤其是地震報告的完整參數(規模、深度、各地震度),這是做地震速報摘要的關鍵原料。
先澄清一個常見誤解:氣象署開放資料平台仍然開放、授權碼免費(寫作當下查證過;2023 年氣象局改制為氣象署時換過網域,可能因此有「不開放了」的印象)。
CWA_API_KEY。| 資料集編號 | 內容 | 用途 |
|---|---|---|
| E-A0015-001 | 顯著有感地震報告 | 地震速報摘要 |
| E-A0016-001 | 小區域有感地震報告 | 補充小地震 |
| W-C0033-001 | 天氣特報(各縣市) | 豪雨、強風等特報 |
| O-A0001-001 | 自動氣象站觀測 | 之後做在地化資訊 |
(實作時請在平台的資料列表逐一確認編號仍有效,資料集偶有調整。)
import os
import ssl
import urllib.parse
import urllib.request
import json
BASE = "https://opendata.cwa.gov.tw/api/v1/rest/datastore"
# 實測發現:CWA 的政府憑證鏈缺 Subject Key Identifier 欄位(用來識別憑證所屬金鑰的欄位),
# Python 3.13 起預設開啟的 VERIFY_X509_STRICT(對憑證做額外嚴格檢查的設定)
# 會直接拒絕連線,
# 需在 context 關閉 strict 檢查(憑證本身仍有驗證)
ctx = ssl.create_default_context()
ctx.verify_flags &= ~ssl.VERIFY_X509_STRICT
def cwa_get(dataset: str, **params) -> dict:
params["Authorization"] = os.environ["CWA_API_KEY"].strip()
url = f"{BASE}/{dataset}?{urllib.parse.urlencode(params)}"
with urllib.request.urlopen(url, timeout=30, context=ctx) as resp:
return json.load(resp)
# 最近的顯著有感地震
quakes = cwa_get("E-A0015-001", limit=5)
for eq in quakes["records"]["Earthquake"]:
info = eq["EarthquakeInfo"]
print(info["OriginTime"], f"規模 {info['EarthquakeMagnitude']['MagnitudeValue']}",
f"深度 {info['FocalDepth']} km", info["Epicenter"]["Location"])
地震報告的 JSON 結構層層巢狀(震央、規模、深度、各縣市震度分別在不同層),建議先把「震央位置、規模、深度、最大震度、發震時間」五個欄位整理成單層、容易取用的字典(flat dict),之後交給 Gemini 或存進 Firestore 都比較方便:
LEVELS = ["1級", "2級", "3級", "4級", "5弱", "5強", "6弱", "6強", "7級"]
def flatten(eq: dict) -> dict:
info = eq["EarthquakeInfo"]
areas = [a for a in eq["Intensity"]["ShakingArea"]
if a["AreaIntensity"] in LEVELS]
top = max((a["AreaIntensity"] for a in areas), key=LEVELS.index)
counties = []
for a in areas:
if a["AreaIntensity"] == top:
for name in a["CountyName"].split("、"):
if name not in counties:
counties.append(name)
return {
"origin_time": info["OriginTime"],
"location": info["Epicenter"]["Location"],
"magnitude": info["EarthquakeMagnitude"]["MagnitudeValue"],
"depth_km": info["FocalDepth"],
"max_intensity": top,
"max_intensity_counties": counties,
}
震度不能直接當數字比大小,因為有「5弱」「5強」這種等級,所以先列出由小到大的順序(LEVELS),再找出最大的一級和對應的縣市。
實測結果(2026-09-17):上面的程式可直接執行,欄位名稱全部正確。最近五筆顯著有感地震的輸出如下:
2026-09-14T06:44:41+08:00 規模 4.9 深度 10.1 km 臺東縣政府東南東方 43.0 公里 (位於臺灣東南部海域)
2026-09-14T03:21:35+08:00 規模 4.7 深度 17.6 km 花蓮縣政府南南西方 54.9 公里 (位於花蓮縣萬榮鄉)
2026-09-11T19:24:56+08:00 規模 4.5 深度 24.6 km 花蓮縣政府南方 67.9 公里 (位於臺灣東部海域)
2026-09-07T00:37:01+08:00 規模 4.8 深度 32.1 km 臺東縣政府東南東方 23.8 公里 (位於臺灣東南部海域)
2026-09-04T17:11:00+08:00 規模 4.5 深度 13.7 km 臺東縣政府東南東方 44.9 公里 (位於臺灣東南部海域)
用 timeFrom/timeTo 兩個參數也查得到歷史報告,例如 8 月 22 日那筆規模 5.1 的東部海域地震。報告最外層的 ReportContent 欄位就是官方的一句話摘要(「08/22-11:40臺灣東部海域發生規模5.1有感地震,最大震度宜蘭縣南澳、花蓮縣和平、宜蘭縣宜蘭市3級。」)。各縣市的最大震度則放在 Intensity 底下的 ShakingArea 陣列裡,逐一列出。官方雖然已經有一句話摘要,但之後要交給 Gemini 改寫和分級的是原始的數值,所以單層字典只保留參數。查詢這一筆並交給上面的 flatten():
aug = cwa_get("E-A0015-001", timeFrom="2026-08-22T00:00:00", timeTo="2026-08-23T00:00:00")
print(flatten(aug["records"]["Earthquake"][0]))
得到(9/19 執行):
{"origin_time": "2026-08-22T11:40:15+08:00", "location": "宜蘭縣政府東南方 41.7 公里 (位於臺灣東部海域)",
"magnitude": 5.1, "depth_km": 27.8, "max_intensity": "3級", "max_intensity_counties": ["宜蘭縣", "花蓮縣"]}
W-C0033-001 同樣正常,9/17 查詢時苗栗、臺中、彰化、屏東、澎湖五縣市有陸上強風特報生效;沒有特報時該縣市會回傳空的 hazards 陣列,程式要處理這個情況。
唯一的問題是上面註解寫的 SSL 憑證檢查,查了一陣子才確認原因不在授權碼。
兩者用「事件時間+地點」比對:NCDR 出現地震示警時,去 CWA 查詢完整地震報告補充細節。
明天 Day 7 是第一次週回顧:把兩個資料源整合成一條資料流程,定稿整體設計。